Transactional

@VERO
Created Date · 2023년 05월 29일 10:05
Last Updated Date · 2023년 10월 06일 02:10

선언적 트랜잭션이란

트랜잭션의 동작을 명시적으로 정의하지 않고, 선언적인 방식으로 트랜잭션을 처리하는 개념이다. 주로 프레임워크나 라이브러리 수준에서 정의된다.

개발자가 명시적으로 트랜잭션을 관리하는 대신, 프레임워크에게 책임을 위임하여 간단한 설정이나 어노테이션으로 트랜잭션을 선언하고 처리한다.

@Transactional 이란

Spring의 선언적 트랜잭션 방식 중 하나이다.

트랜잭션을 적용하고자 하는 클래스나 메서드 위에 @Transactional 을 선언하면 트랜잭션 기능이 적용된 프록시 객체가 생성되고, @Transactional 이 포함된 메서드가 호출되는 경우 PlatformTransactionManager 를 사용하여 트랜잭션을 시작한다.

propagation 옵션

트랜잭션이 진행중일 때 추가 트랜잭션 진행을 어떻게 할 지 결정하는 것을 트랜잭션의 전파 설정이라고 한다.

물리 트랜잭션 vs 논리 트랜잭션

1개의 트랜잭션을 사용한다는 것은 하나의 Connection 객체를 사용한다는 것이다. 트랜잭션이 실제 데이터베이스의 트랜잭션을 사용한다는 점에서 물리 트랜잭션이라고 한다. 커넥션을 통해 커밋/롤백하는 단위이다.

전파 속성에 따라 외부 트랜잭션과 내부 트랜잭션이 동일한 트랜잭션을 사용할 수도 있다. 스프링에서는 실제 데이터베이스 트랜잭션과 스프링이 처리하는 트랜잭션을 구분하기 위해 논리 트랜잭션이라는 개념이 등장했다.

트랜잭션 전파 없이 1개의 트랜잭션만 사용되면 물리 트랜잭션만 존재하고, 트랜잭션 전파가 사용될 때 논리 트랜잭션 개념이 사용된다.

종류

  • REQUIRED
    • 기본값. 부모 트랜잭션이 존재하는 경우 참여하고, 존재하지 않는 경우 트랜잭션을 새롭게 생성한다.
    • 자식에서 발생한 예외는 부모 트랜잭션도 롤백시킨다.
    • 부모 트랜잭션이 커밋되면 자식의 모든 변경사항도 함께 커밋된다.
  • SUPPORTS
    • 부모 트랜잭션이 존재하는 경우 참여하고, 없는 경우 non-transactional 상태로 실행한다.
    • 자식에서 발생한 예외는 부모 트랜잭션도 롤백시킨다.
    • 부모 트랜잭션이 커밋되면 자식의 모든 변경사항도 함께 커밋된다.
  • MANDATORY
    • 부모 트랜잭션이 있으면 참여하고, 없는 경우 예외를 발생시킨다.
    • 자식에서 발생한 예외는 부모 트랜잭션도 롤백시킨다.
    • 부모 트랜잭션이 커밋되면 자식의 모든 변경사항도 함께 커밋된다.
  • REQUIRES_NEW
    • 부모 트랜잭션은 일시 중단되고, 자식은 새로운 트랜잭션을 생성한다.
    • 부모/자식 트랜잭션의 롤백 여부는 서로 영향을 주지 않는다. 즉, 자식 트랜잭션의 롤백은 부모 트랜잭션에 영향을 주지 않는다.
    • 내부 트랜잭션이 롤백되는 경우는 실제 Connection에 롤백을 호출하는 경우이므로 트랜잭션이 종료된다.
    • 자식 트랜잭션은 부모 트랜잭션과 독립적으로 커밋되거나 롤백된다.
  • NOT_SUPPORTED
    • non-transactional 상태로 실행하며, 부모 트랜잭션이 존재하는 경우 일시 정지 시킨다.
    • 부모 트랜잭션의 커밋은 자식의 동작에 영향을 주지 않는다.
  • NEVER
    • non-transactional 상태로 실행하며, 부모 트랜잭션이 존재하는 경우 예외를 발생시킨다.
    • 부모 트랜잭션의 커밋은 자식의 동작에 영향을 주지 않는다.
  • NESTED
    • 부모 트랜잭션과는 별개의 중첩된 트랜잭션을 만든다.
    • 부모 트랜잭션이 없는 경우에는 새로운 트랜잭션을 생성한다.
    • 부모 트랜잭션이 커밋된 경우 -> 자식 트랜잭션의 변경 사항은 아직 데이터베이스에 확정되지 않는다. 자식 트랜잭션은 별도로 커밋되거나 롤백되어야 한다.
    • 부모 트랜잭션이 롤백된 경우 -> 자식 트랜잭션도 롤백된다.
    • 자식 트랜잭션이 커밋된 경우 -> 부모 트랜잭션은 커밋되지 않는다.
    • 자식 트랜잭션이 롤백된 경우 -> 자식 트랜잭션만 롤백되며 부모 트랜잭션은 롤백되지 않는다.
    • NESTED 트랜잭션은 데이터베이스의 세이브포인트 메커니즘을 사용하여 중첩된 트랜잭션의 시작 지점을 표시한다. 롤백 발생 시, 해당 세이브포인트까지만 롤백된다. 즉, NESTED 트랜잭션을 사용하려면 데이터베이스와 JDBC 드라이버가 세이브포인트를 지원해야 한다.

참고

readOnly 옵션

기본적으로는 false로 설정되어 있다.

true로 설정하는 경우: 데이터에 대해 Lock을 적용할 필요가 없고, 접근할 수 있는 데이터(스냅샷, 튜플 등)가 변경되지 않기 때문에 일관적인 데이터를 읽어오고 제공할 수 있다.

조회 작업만 수행한다는 것을 명시적으로 나타낼 수 있다.

MySQL에서의 readOnly

SELECT 문에 대해서만 기능을 지원하고, Transaction ID 설정에 대한 오버헤드를 해결할 수 있다. (MVCC) 즉, Read Only 트랜잭션에 대해서는 Transaction ID가 부여되지 않는다.

별도의 스냅샷을 통해 데이터를 조회하기 때문에 데이터 일관성을 보장할 수 있다. 데이터 변경과 같이 조회가 아닌 작업을 하는 경우 에러를 발생시킨다.

  • 스냅샷 : 데이터 수정 요청이 전달되었을 때, 변경되는 데이터를 따로 저장해두는 것.

isolation 옵션

  • DEFAULT: 기본적으로 DEFAULT 옵션이 적용되어 있다. 사용하는 DB의 기본 격리 수준을 따른다.
  • READ_UNCOMMITTED : 커밋되지 않은 데이터를 다른 트랜잭션에서 접근 가능하다.
  • READ_COMMITTED : 매 조회가 이루어질 때마다 새로운 스냅샷을 뜨기 때문에 다른 트랜잭션이 커밋하면 (변경 사항이 존재하면) 변경된 데이터를 볼 수 있다. 즉, B 트랜잭션이 끝나기 전에 A 트랜잭션에 의해 데이터가 변경되면 다시 조회할 때 변경된 데이터를 읽게 된다.
  • REPEATABLE__READ : 조회한 데이터에 대해 Shared Lock이 걸려서 다른 트랜잭션이 새로운 데이터를 추가할 수 있다. A 트랜잭션이 시작하고 처음 조회한 데이터의 스냅샷을 저장하고, 이후에 동일한 쿼리를 호출하면 해당 스냅샷에서 데이터를 가져온다. 즉, B 트랜잭션이 새로 커밋해도 (변경 사항이 있어도) A 트랜잭션이 조회하는 데이터는 변하지 않는다.
  • SERIALIZED : 읽기 작업에도 락을 걸어 여러 트랜잭션이 동시에 같은 데이터에 접근할 수 없다. 가장 안전하지만 성능 저하가 크기 때문에 자주 사용되지 않는다.

ProxyMode

프록시를 생성하는 방식을 제어하는데 사용되는 옵션이다.

  1. ProxyMode.DEFAULT
    • 스프링이 프록시 모드를 결정한다.
    • 인터페이스를 구현하고 있는 경우에는 ProxyMode.PROXY를, 구현하지 않는 경우에는 ProxyMode.TARGET_CLASS를 사용한다.
  2. ProxyMode.PROXY
    • JDK 기반의 다이나믹 프록시를 생성한다.
    • 대상 객체가 인터페이스를 구현하고 있어야 한다.
    • 대상 객체의 인터페이스 메서드 호출 시 프록시를 거쳐 AOP 동작을 호출한다.
  3. ProxyMode.ASPECTJ
    • AspectJ를 사용하여 프록시를 생성한다.
    • 인터페이스를 구현하고 있지 않아도 된다.
    • 컴파일 타임에 AspectJ

갑자기 무슨 인터페이스라는 말인가...

스프링 AOP에서 ProxyMode.PROXY를 사용하는 경우, 대상 객체는 인터페이스를 구현하고 있어야 합니다. 이는 JDK 다이나믹 프록시를 사용하여 프록시 객체를 생성하기 때문입니다. 다이나믹 프록시는 인터페이스의 구현을 바탕으로 프록시 객체를 생성하므로, 대상 객체가 인터페이스를 구현하지 않으면 프록시를 생성할 수 없습니다. by G-선생